﻿using System;
using System.IO;
using System.Text;
using System.Threading;

namespace DZTC.Base32Utility.Core.Base32
{
    public static class Base32Encoder
    {
        internal static readonly byte[] ENCODE_TABLE = Encoding.ASCII.GetBytes("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567");
        private const byte PAD = (byte) '=';

        private static int GetPadCount(int quantumLength)
        {
            switch (quantumLength)
            {
                case 5:
                case 0:
                    return 0;

                case 4:
                    return 1;
                case 3:
                    return 3;
                case 2:
                    return 4;
                case 1:
                    return 6;
            }

            throw new ArgumentException("quantumLength");
        }

        private static long GetDestinationLength(long inputLength)
        {
            var r = inputLength%5 != 0 ? 8 : 0;
            return (inputLength/5)*8 + r;
        }

        
        private static byte[] EncodeArrayEndWithPadding(byte[] input, long position, int len)
        {
            if (len < 0 || len > 5)
                throw new ArgumentException("len");

            var lastBytes = new byte[5];
            Array.Copy(input, position, lastBytes, 0, len);

            // aligned array
            var output = EncodeArray(lastBytes, 5);

            for (var i = 0; i < GetPadCount(len); i++)
                output[7 - i] = PAD;

            return output;
        }

        private static byte[] EncodeArray(byte[] input, long length)
        {
            var output = new byte[GetDestinationLength(length)];
            var spos = 0L;
            var dpos = 0L;

            var upBound = length - length%5;

            while (spos < upBound)
            {
                var number = ((long) input[spos + 0] << 32) + ((long) input[spos + 1] << 24) +
                             ((long) input[spos + 2] << 16) + ((long) input[spos + 3] << 8) +
                             ((long) input[spos + 4] << 0);


                output[dpos + 0] = ENCODE_TABLE[(number >> 35) & 0x1f];
                output[dpos + 1] = ENCODE_TABLE[(number >> 30) & 0x1f];
                output[dpos + 2] = ENCODE_TABLE[(number >> 25) & 0x1f];
                output[dpos + 3] = ENCODE_TABLE[(number >> 20) & 0x1f];
                output[dpos + 4] = ENCODE_TABLE[(number >> 15) & 0x1f];
                output[dpos + 5] = ENCODE_TABLE[(number >> 10) & 0x1f];
                output[dpos + 6] = ENCODE_TABLE[(number >> 05) & 0x1f];
                output[dpos + 7] = ENCODE_TABLE[(number >> 00) & 0x1f];

                spos += 5;
                dpos += 8;
            }

            var endLen = (int) (length - spos);
            if (endLen > 0) // need padding
            {
                var lastBytes = EncodeArrayEndWithPadding(input, spos, endLen);
                Array.Copy(lastBytes, 0, output, dpos, 8);
            }

            return output;
        }

        private static void EncodeInternal(Stream inputStream, Stream outputStream,
                                           CancellationToken ct,
                                           ProgressReporter pr)
        {
            pr.SetMax(inputStream.Length);

            var encodedBytes = 0L;
            var buffer = new byte[5000*1024];

            while (true)
            {
                ct.ThrowIfCancellationRequested();
                var len = inputStream.Read(buffer, 0, buffer.Length);
                if (len <= 0) break;


                ct.ThrowIfCancellationRequested();
                var array = EncodeArray(buffer, len);

                ct.ThrowIfCancellationRequested();
                outputStream.Write(array, 0, array.Length);

                encodedBytes += len;
                pr.Report(encodedBytes);
            }

            outputStream.Flush();
        }


        public static void Encode(Stream inputStream, Stream outputStream,
                                  CancellationToken ct = default(CancellationToken),
                                  ProgressReporter pr = null)
        {
            if (inputStream == null)
                throw new ArgumentNullException("inputStream");

            if (outputStream == null)
                throw new ArgumentNullException("outputStream");

            if (!inputStream.CanRead)
                throw new ArgumentException("Input stream must be readable");

            if (!inputStream.CanSeek)
                throw new ArgumentException("Input stream must be seekable");

            if (!outputStream.CanWrite)
                throw new ArgumentException("Input stream must be writeable");

    
            pr = pr ?? ProgressReporter.None;

            EncodeInternal(inputStream, outputStream, ct, pr);
        }
    }
}